Skip to content

fix: Numeric value clearing in preset input controls#77139

Merged
aaronrobertshaw merged 3 commits intoWordPress:trunkfrom
DarkMatter-999:fix/preset-input-control-clear-empty-numeric-value
Apr 10, 2026
Merged

fix: Numeric value clearing in preset input controls#77139
aaronrobertshaw merged 3 commits intoWordPress:trunkfrom
DarkMatter-999:fix/preset-input-control-clear-empty-numeric-value

Conversation

@DarkMatter-999
Copy link
Copy Markdown
Contributor

@DarkMatter-999 DarkMatter-999 commented Apr 8, 2026

What?

Closes #77124

Fixes a bug where clearing numeric values in shared preset-based controls (used by inputs like margin and border radius) could retain a partial value (for example, 6 after deleting 60).

Why?

When users clear numeric values with backspace in affected block controls, the intermediate partial value may be applied and persisted instead of the field becoming fully empty. This creates incorrect style values and inconsistent UX.

How?

Updates PresetInputControl custom value change handling so empty input is treated as a valid cleared state instead of being discarded as undefined. This allows the control to propagate a true empty value and avoids persisting transient partial numeric states during deletion.

Testing Instructions

Testing Instructions for Keyboard

  1. Open the editor and insert a block (for example, Paragraph).
  2. In block settings, set a numeric value like 60 in controls backed by preset input (e.g. Margin, Padding).
  3. Place the cursor in the numeric field and press backspace until cleared.
  4. Confirm the value is fully cleared (empty), and no partial value like 6 is retained.
  5. Repeat for at least one additional affected control to confirm shared behavior.

Screenshots or screencast

Before

number-input.mov

After

number-input-fix.mov

Use of AI Tools

I used GitHub Copilot as an assistive drafting and code-review aid for the PR.

@github-actions github-actions bot added the [Package] Block editor /packages/block-editor label Apr 8, 2026
@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 8, 2026

👋 Thanks for your first Pull Request and for helping build the future of Gutenberg and WordPress, @DarkMatter-999! In case you missed it, we'd love to have you join us in our Slack community.

If you want to learn more about WordPress development in general, check out the Core Handbook full of helpful information.

@github-actions github-actions bot added the First-time Contributor Pull request opened by a first-time contributor to Gutenberg repository label Apr 8, 2026
@DarkMatter-999 DarkMatter-999 changed the title fix: input control clear empty numeric value fix: Numeric value clearing in preset input controls Apr 8, 2026
@DarkMatter-999 DarkMatter-999 marked this pull request as ready for review April 8, 2026 10:55
@DarkMatter-999 DarkMatter-999 requested a review from ellatrix as a code owner April 8, 2026 10:55
@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 8, 2026

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: DarkMatter-999 <lakshyajeet@git.wordpress.org>
Co-authored-by: aaronrobertshaw <aaronrobertshaw@git.wordpress.org>
Co-authored-by: dpmehta <mehtadev@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@Mamaduka Mamaduka requested a review from aaronrobertshaw April 8, 2026 12:40
@Mamaduka Mamaduka added the [Type] Bug An existing feature does not function as intended label Apr 8, 2026
Copy link
Copy Markdown
Contributor

@aaronrobertshaw aaronrobertshaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for putting up a PR for this @DarkMatter-999 👍

The proposed fix addresses the original bug, but it introduces another very minor issue. It persists empty string values in the block attributes.

I think we can simplify the change here a little and both fix the bug and avoid persisting empty strings and unnecessary block attributes.

It would also help here to have some additional tests to protect against regressions here and confirm the fix.

Here's a proposed diff that would simplify the fix, protect against the empty string attribute value, and define some tests for the control
diff --git a/packages/block-editor/src/components/preset-input-control/index.js b/packages/block-editor/src/components/preset-input-control/index.js
index dbafef496b3..2d6df63f334 100644
--- a/packages/block-editor/src/components/preset-input-control/index.js
+++ b/packages/block-editor/src/components/preset-input-control/index.js
@@ -168,22 +168,20 @@ export default function PresetInputControl( {
 		unitConfig?.max ?? customValueSettings[ computedUnit ]?.max ?? 10;
 
 	const handleCustomValueChange = ( newValue ) => {
-		const isExplicitlyEmpty = newValue === '';
-		const isNumeric =
-			newValue !== undefined && ! isNaN( parseFloat( newValue ) );
-
-		let newCustomValue;
-		if ( isExplicitlyEmpty ) {
-			newCustomValue = '';
-		} else if ( isNumeric ) {
-			newCustomValue = newValue;
-		} else {
-			newCustomValue = undefined;
+		// Treat empty/undefined input as an explicit clear and propagate
+		// undefined so the attribute is removed rather than being persisted
+		// as an empty string.
+		if ( newValue === undefined || newValue === '' ) {
+			onChange( undefined );
+			return;
 		}
 
-		if ( newCustomValue !== undefined ) {
-			onChange( newCustomValue );
+		// Ignore non-numeric intermediate input (e.g. just a unit).
+		if ( isNaN( parseFloat( newValue ) ) ) {
+			return;
 		}
+
+		onChange( newValue );
 	};
 	const handleCustomValueSliderChange = ( next ) => {
 		onChange( [ next, computedUnit ].join( '' ) );
diff --git a/packages/block-editor/src/components/preset-input-control/test/index.js b/packages/block-editor/src/components/preset-input-control/test/index.js
index b46ca23d2e4..8252c727e5b 100644
--- a/packages/block-editor/src/components/preset-input-control/test/index.js
+++ b/packages/block-editor/src/components/preset-input-control/test/index.js
@@ -92,6 +92,76 @@ describe( 'PresetInputControl', () => {
 		expect( mockOnChange ).toHaveBeenCalledWith( '25px' );
 	} );
 
+	it( 'clears value with undefined when input is fully erased via backspace', () => {
+		render(
+			<PresetInputControl
+				{ ...defaultProps }
+				presets={ presets }
+				value="60px"
+				disableCustomValues={ false }
+			/>
+		);
+
+		const input = screen.getByRole( 'spinbutton' );
+
+		// Simulate the bug scenario: backspace through "60" one character
+		// at a time. fireEvent.change is used here (rather than userEvent
+		// keyboard interactions) because the controlled UnitControl input
+		// does not respond to synthesised key events in jsdom. The
+		// intermediate "6" forwarding is expected and correct; the bug
+		// was that the final clear silently failed to propagate, leaving
+		// the parent stuck on the partial value.
+		fireEvent.change( input, { target: { value: '6' } } );
+		fireEvent.change( input, { target: { value: '' } } );
+
+		// The final clear must propagate as undefined, not be swallowed,
+		// and never be persisted as an empty string.
+		expect( mockOnChange ).toHaveBeenLastCalledWith( undefined );
+		expect( mockOnChange ).not.toHaveBeenCalledWith( '' );
+	} );
+
+	it( 'clears value with undefined when input is cleared in one shot', async () => {
+		const user = userEvent.setup();
+
+		render(
+			<PresetInputControl
+				{ ...defaultProps }
+				presets={ presets }
+				value="25px"
+				disableCustomValues={ false }
+			/>
+		);
+
+		const input = screen.getByRole( 'spinbutton' );
+		await user.clear( input );
+
+		expect( mockOnChange ).toHaveBeenCalledWith( undefined );
+		expect( mockOnChange ).not.toHaveBeenCalledWith( '' );
+	} );
+
+	it( 'does not call onChange for non-numeric intermediate input', async () => {
+		const user = userEvent.setup();
+
+		render(
+			<PresetInputControl
+				{ ...defaultProps }
+				presets={ presets }
+				value="15px"
+				disableCustomValues={ false }
+			/>
+		);
+
+		const input = screen.getByRole( 'spinbutton' );
+		await user.clear( input );
+		mockOnChange.mockClear();
+
+		// Typing a stray non-numeric character should not propagate a change.
+		await user.type( input, 'a' );
+
+		expect( mockOnChange ).not.toHaveBeenCalledWith( 'a' );
+		expect( mockOnChange ).not.toHaveBeenCalledWith( 'apx' );
+	} );
+
 	it( 'uses select dropdown for many presets', async () => {
 		const manyPresets = Array.from( { length: 12 }, ( _, i ) => ( {
 			name: `Preset ${ i + 1 }`,


let newCustomValue;
if ( isExplicitlyEmpty ) {
newCustomValue = '';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passing this through to onChange will result in empty string values in the block attributes persisted in the block comment delimiter. It would be best if we can fix this bug without doing this.

@DarkMatter-999
Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review and for providing the test cases, @aaronrobertshaw !

I’ve updated the PR to use the simplified logic you suggested. I’ve also included the new tests to protect against future regressions.

Ready for another look!

Copy link
Copy Markdown
Contributor

@aaronrobertshaw aaronrobertshaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the quick iteration here @DarkMatter-999 👍

The simplified logic reads much more clearly and the new tests give us good coverage against regressions. Testing as advertised for me.

✅ Clearing a numeric input via backspace no longer leaves it stuck on the partial value
✅ Fully cleared input passes undefined so no empty string value is persisted
✅ Unit-only intermediate input value is ignored as expected
✅ New unit tests all passing locally

LGTM 🚢

@aaronrobertshaw aaronrobertshaw merged commit ccf1c29 into WordPress:trunk Apr 10, 2026
40 checks passed
@github-actions github-actions bot added this to the Gutenberg 23.0 milestone Apr 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

First-time Contributor Pull request opened by a first-time contributor to Gutenberg repository [Package] Block editor /packages/block-editor [Type] Bug An existing feature does not function as intended

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Numeric input fields retain partial values when cleared via backspace across multiple controls

3 participants